Programação Web

Aula 22 - Introdução ao Node.js




Helder Jefferson Ferreira da Luz

helder.luz@ifpr.edu.br

Objetivos

  • Entender o que é o Node.js e seu ecossistema.
  • Aprender a criar um servidor web básico.
  • Compreender os princípios de APIs REST.
  • Utilizar o framework Express.js para criar rotas.
  • Conectar uma aplicação Node.js a um banco de dados MySQL.

Node.js

É um ambiente de execução JavaScript multiplataforma.


Ele utilize o motor JavaScript V8, utilizado no Google Chrome.


Composto por um conjunto de módulos que oferecem diversas funcionalidades.

Node.js

Criado por Ryan Dahl e lançado em 2009.


Ele saiu do desenvolvimento do Node.js em 2012.


Atualmente é mantida pela OpenJS Foundation.


O que é recomendado saber antes de aprender Node.js?

  • Lexical Structure
  • Expressions
  • Data Types
  • Classes
  • Variables
  • Functions
  • this operator
  • Arrow Functions
  • Loops
  • Scopes
  • Arrays
  • Template Literals
  • Strict Mode
  • ECMAScript 2015 (ES6) and beyond
  • Asynchronous JavaScript

Instalação e versão

Download: https://nodejs.org/

Há duas versões:

  • Impar (current)
  • Par (current | active | maintenance)

Para verificar a versão do node no terminal:

node --version

Instalação e versão

Hello World

const { createServer } = require('http'); // Importa o módulo HTTP
const server = createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/html' });
  res.end('Hello World!');
});

server.listen(8080, () => { // Inicia o servidor na porta 8080
  console.log('Server running at http://localhost:8080/');
});
O programa em node deve ser executado no terminal:
C:\Users\Your Name> node hello.js

Hello World

A aplicação estará esperando por requisição na porta 8080.


Para requisitar à aplicação, acesse no navegador:
http://localhost:8080

Módulos

São equivalentes a bibliotecas em outras linguagens.
Possuem um conjunto de funções para você usar na sua aplicação.

O Node.js possui diversos módulos embutidos que não exigem instalação.
Lista: Node.js Built-in Modules (w3schools.com)

Usaremos também alguns módulos externos, que precisarão ser instalados.

Módulos

Há duas formas de utilizar módulos no Node.js:

Módulos - CommonJS

É o formato utilizado por padrão pelo Node.js.

const http = require('http');

Isso importará o módulo http para criar um servidor.

Módulos - ES6

Padrão de importação de módulos do JavaScript, lançado em 2015.

import http from 'http';

Módulos - ES6

Para esse formato, é necessário dizer ao Node.js que estará usando-o.

Para isso, informe no arquivo package.json do projeto.

{
    "type": "module"
}

Ou utilize a extensão .mjs ao invés de .js.

Criar módulos – ES6

Você pode criar seu próprio módulo e exportar as funções necessárias usando a palavra export.

datas.js
function mes(){
  return 'Mês ' + (new Date()).getMonth();
}
function dia(){
  return 'Dia ' + (new Date()).getDay();
}
export { mes, dia };

Criar módulos – ES6

Utilizando o módulo criado:

demoModulo.js
import { createServer } from 'http';
import { mes, dia } from './datas.js';

const server = createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
  res.write('Data: ' + mes() + ', ' + dia());
  res.end();
})

server.listen(8080);

Módulo HTTP

Permite criar um servidor HTTP que houve as portas do servidor e responde ao cliente.

O método createServer() cria um servidor HTTP.

exemploHTTP.js
import http from 'http';

//cria um objeto servidor
http.createServer((req, res) => {
  res.write('Hello World'); //escreve uma resposta para o cliente
  res.end(); //finaliza a resposta e envia
}).listen(8080); //o objeto servidor ouve a porta 8080

Módulo HTTP - cabeçalho

Pode-se passar um cabeçalho usando o método writeHead().
O primeiro argumento é o código do status (200 é OK), o segundo é o objeto contendo o cabeçalho.

exemploHTTP.js
import http from 'http';

http.createServer((req, res) => {
  //informa que a resposta precisa ser apresentada como HTML
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.write('Hello World'); 
  res.end();
}).listen(8080);

Módulo HTTP - createServer

http.createServer((req, res) => {})

O argumento req representa a requisição do cliente.
É um objeto do tipo http.IncomingMessage que possui diversas propriedades:

  • method
  • url
  • statusCode

Gerenciador de pacote NPM

A instalação do Node.js inclui o NPM (Node Package Manager).

É o gerenciador de pacotes para o Node que permite instalar e gerenciar os pacotes (módulos) do seu projeto.

Para instalar um pacote:

npm install upper-case

Irá criar uma pasta node_modules com os arquivos do pacote.

Gerenciador de pacote NPM

exemploNPM.js
import http from 'http';
import uc from 'upper-case';

http.createServer((req, res) => {
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.write(uc.upperCase('hello world!'));
  res.end();
}).listen(8080);

Gerenciador de pacote NPM

No arquivo package.json ele irá adicionar a dependência instalada.

package.json
{
    "type": "module",
    "dependencies": {
        "upper-case": "^2.0.2"
    }
}

Com base nele, é possível instalar todas as dependências de um projeto usando:

npm install

Gerenciador de pacote NPM

Lista de pacotes:
https://www.npmjs.com

Node – package.json

É possível criar o package.json inicial usando o comando

npm init

Ele irá fazer algumas perguntas para a criação do arquivo, com informações como: nome, versão, descrição, arquivo principal, scripts de teste, autor e licença.


Pode-se pular essa etapa e criar um arquivo padrão usando:

npm init -y

Nodemon

Auxilia no processo de desenvolvimento de aplicações, reiniciando o servidor automaticamente quando ocorre modificações nos arquivos.

Instalação global:

npm install -g nodemon

Execução

nodemon server.js

Nodemon

Instalação local:

npm install nodemon --save-dev

Execução local:

npx nodemon server.js

Em uma instalação local será adicionado no package.json:

"devDependencies": {
  "nodemon": "^3.1.11"
}

Nodemon

Pode-se utilizar um script para iniciar a execução.
No package.json:

"scripts": {
  "start": "npx nodemon server.js"
}

Para executar o script:

npm start

REST

REpresentational State Transfer

REST

REpresentational State Transfer (Transferência Representacional de Estado).


É um estilo de arquitetura de software para criação de serviços web.


Foi descrito em 2000 por Roy Fielding.


API REST

Uma API REST deve seguir 6 princípios:

  1. Interface uniforme
  2. Desacoplamento do cliente-servidor
  3. Sem estado definido
  4. Capacidade de armazenamento em cache
  5. Arquitetura de sistema em camadas
  6. Código sob demanda (opcional)

Uma API que segue todos os principal é também chamada de RESTful.

REST - Interface uniforme

Baseado em recursos
Os recursos são identificados na requisição usando a URI como identificador:
E.g. localhost:8080/contatos

Manipulação dos recursos pela representação
O cliente acessa o recurso pela representação recebida (JSON, XML, etc) e tem informação necessária para modificar ou deletar o recurso do servidor.

Mensagens auto descritivas
Cada mensagem inclui informação suficiente para descrever como processar a mensagem
E.g. {'Content-Type': 'application/json'}

REST

Desacoplamento do cliente-servidor
O cliente e servidor são completamente independentes um do outro.

Sem estado definido
Cada solicitação precisa incluir todas as informações necessárias para processa-lo. Não deve requerer nenhuma sessão do lado do servidor.

Capacidade de armazenamento em cache
Quando possível, os recursos devem ser armazenados em cache pelo cliente ou servidor.

REST

Arquitetura de sistema em camadas
As requisições podem passar por diferentes camadas mas, tanto o cliente quanto o servidor não devem ser capazes de dizer se há intermediários.

Código sob demanda
Pode enviar código executável para o cliente (e.g. applets Java)

REST - HTTP

Todas as requisições são feitas usando o protocolo HTTP.

Os métodos HTTP mais comuns são:

  • GET – Requisitar informação
  • PUT – Modificar um registro
  • POST – Inserir um registro
  • DELETE – Remover um registro
  • PATCH – Modificar parcialmente um registro

Fetch

Por padrão, o fetch utiliza o método GET, mas pode-se escolher o método especificando no header.

const header = {
  method: 'POST',
  headers: {
    'Content-type': 'application/json; charset=UTF-8'
  },
  body: JSON.stringify({
    nome: nome,
    idade: idade,
    patrimonio: patrimonio.replace(',','.')
  })
}
let resposta = await fetch('/contatos', header);
resposta = await resposta.json();

HTTP - Ferramentas de teste

Pode-se testar a resposta da sua API REST a requisições HTTP sem precisar implementar uma página ou script JS.

Há diversas ferramentas para teste, dentre elas:

HTTP - Ferramentas de teste

Exemplo de código do REST Client

GET http://localhost:8080

###
GET http://localhost:8080/contatos

###
POST http://localhost:8080/contato HTTP/1.1
Content-Type: application/json

{
    "nome": "Helder",
    "data_nasc": "1990-02-02",
    "email": "helderjfl@gmail.com"
}

HTTP - código de status

A resposta a uma requisição HTTP informa um código, indicando o status da requisição.


Eles são agrupados como:

  • 1xx informativo
  • 2xx sucesso
  • 3xx redirecionamento
  • 4xx erro no cliente
  • 5xx erro no servidor

HTTP - código de status

Os principais códigos são:

  • 200 Ok
  • 201 Created Criado
  • 202 Accepted Aceito
  • 400 Bad request
  • 401 Unauthorized Não autorizado
  • 403 Forbbiden Proibido
  • 404 Not found Não encontrado
  • 418 I'm a teapot
  • 500 Internal server error Erro interno do servidor

Express

É possível criar um API REST utilizando Node.js puro.

Entretanto, o framework Express facilita o processo de criação.

Ele traz manipuladores para:

  • Verbos HTTP (GET, POST,...)
  • Rotas (URI)
  • Arquivos estáticos
  • etc

Express

Instalação

npm install express

Importação do módulo

import express from 'express';

Express

Hello World usando o Express:

import express from 'express'
const app = express();

app.get('/', (req, res) => {
  res.send('Hello World!')
});

app.listen(8080, () => {
  console.log('Servidor rodando')
});

Express - rotas

Criação de rotas para os diferentes tipos de requisições HTTP:

  • app.get
  • app.post
  • app.put
  • app.patch
  • app.delete

Rotas - exemplos

app.get('/clientes', (req, res) => {
  res.status(200).send(database.getClientes())
})

app.get('/clientes/:id', (req, res) => {
  const id = req.params.id;
  res.status(200).send(database.getCliente(id))
})

app.post('/posts', (req, res) => {
  const { nome, email } = req.body;
  console.log(nome, email);
  database.addCliente(nome, email);
  res.status(201).send({'message':'Criado com sucesso'});
})

Express - requisições com JSON

Para tratar requisições com JSON, é necessário configurar um middleware que realiza a análise do conteúdo.

// informa que a requisição pode ser JSON
app.use(express.json()) 

Express - Middleware customizado

Middleware é uma função que intercepta uma requisição antes de ela chegar à sua rota final.
Ele pode modificar a requisição, a resposta, ou simplesmente executar um código.

Exemplo de logger
// Middleware que registra a URL e o método de cada requisição
app.use((req, res, next) => { 
    console.log(`[${new Date().toLocaleString()}] ${req.method} ${req.url}`);
    
    // Chama a próxima função no ciclo da requisição (outro middleware ou a rota)
    next(); 
})

Express - Middleware para arquivos estáticos

O middleware express.static é usado para servir arquivos estáticos (HTML, CSS, imagens) de um diretório. O Express procura os arquivos relativos ao diretório estático.

Exemplo
// Informa ao Express para servir arquivos da pasta 'public'
app.use(express.static('public')); 

// Agora, arquivos em 'public' podem ser acessados diretamente.
// Ex: http://localhost:8080/images/logo.png

MySQL

Para conectar a sua aplicação Node.js ao MariaDB/MySQL há 3 opções de drivers:

Vamos usar o mysql2, que possui suporte a Promise.

npm install mysql2

Importação do módulo

import mysql from 'mysql2/promise';

MySQL

Criação de uma conexão com o banco de dados.
Ao usar await, a conexão é estabelecida e fica pronta para uso, sem a necessidade de um callback connect.

const connection = await mysql.createConnection({
  host: 'localhost',
  user: 'root',
  password: ''
});

console.log('Conexão com o MySQL bem-sucedida!');

MySQL

Consulta ao BD usando o método query.

try {
  await connection.query('CREATE DATABASE IF NOT EXISTS mydbnodeteste');
  console.log('Banco de dados criado ou já existente.');
} catch (err) {
  console.error('Erro ao criar banco de dados:', err);
} finally {
  // É importante fechar a conexão após o uso
  connection.end();
}

MySQL

Conectando a um banco de dados criado

connection = await mysql.createConnection({
  host: 'localhost',
  user: 'aluno',
  password: 'aluno123',
  database: 'exemplo',
  port: '3306'
})

Por padrão a porta é 3306, sendo opcional sua definição.

MySQL

Criação de tabela

const sql = 'CREATE TABLE IF NOT EXISTS clientes (nome VARCHAR(255), endereco VARCHAR(255))';
await connection.query(sql);
console.log('Tabela clientes criada ou já existente.');

Execução de uma query SQL para selecionar os dados

const [rows] = await connection.execute('SELECT * FROM clientes');
console.log('Dados da tabela clientes:', rows);

MySQL

Criação de um registro

const sql = "INSERT INTO clientes (nome, endereco) VALUES ('Company Inc', 'Highway 37')";
const [{ affectedRows }] = await connection.execute(sql);
console.log(`${affectedRows} registro inserido.`);

MySQL

Criação de múltiplos registros

const sql = "INSERT INTO clientes (nome, endereco) VALUES ?";
const dados = [
  ['John', 'Highway 71'],
  ['Peter', 'Lowstreet 4'],
  ['Amy', 'Apple st 652'],
  ['Hannah', 'Mountain 21'],
  ['Michael', 'Valley 345'],
  ['Viola', 'Sideway 1633']
];

const [result] = await connection.query(sql, [dados]);
console.log(`Número de registros inseridos: ${result.affectedRows}`);

MySQL - Connection Pool

Para aplicações reais, não é eficiente criar uma nova conexão para cada consulta.
O Pool de Conexões gerencia múltiplas conexões, reutilizando-as e melhorando a performance e a robustez da aplicação.

// 1. Crie o pool de conexões
const pool = mysql.createPool({
  host: 'localhost',
  user: 'root',
  database: 'exemplo',
  waitForConnections: true,
  connectionLimit: 10, // Limite de conexões no pool
  queueLimit: 0
});

// 2. Use o pool para executar queries
const [rows] = await pool.execute('SELECT * FROM clientes');
console.log('Clientes:', rows);
// Não é necessário fechar a conexão do pool a cada consulta

MySQL - SQL Injection

Ocorre quando um atacante insere uma instrução SQL maliciosa em uma entrada de dados.

Se a aplicação concatena diretamente a entrada do usuário para montar uma query, ela se torna vulnerável.

Exemplo de código vulnerável:

const nome = "Malicioso'); DROP TABLE clientes; --";
const endereco = 'Rua Inexistente';

// NÃO FAÇA ISSO!
const sql = `INSERT INTO clientes (nome, endereco) VALUES ('${nome}', '${endereco}')`;

// A query executada seria:
// INSERT INTO clientes (nome, endereco) VALUES ('Malicioso'); DROP TABLE clientes; --', 'Rua Inexistente')
await connection.query(sql); 

MySQL - Prepared Statements

Para prevenir SQL Injection, deve-se usar prepared statements.

Eles separam a instrução SQL dos dados. Os dados são enviados ao banco de dados de forma segura.

O driver mysql2 suporta isso nativamente usando ? como placeholders.

const sql = "INSERT INTO clientes (nome, endereco) VALUES (?, ?)";
const dados = ['Empresa Segura', 'Avenida Principal, 123'];

// Usando o método execute() com placeholders
const [{ affectedRows }] = await connection.execute(sql, dados);
console.log(`${affectedRows} registro inserido com segurança.`);

Desta forma, a entrada do usuário é tratada como um valor literal e não como parte do comando SQL, prevenindo o ataque.

Mysql

Exemplo de código para o banco de dados
database.js
import mysql from 'mysql2/promise';

const database = {};

database.con = await mysql.createConnection({
    host: 'localhost',
    user: 'root',
    password: '',
    database: 'exemplopatrimonio',
    port: '3306'
})

database.getPessoas = async function (){
    let [rows, fields] = await database.con.execute('SELECT * FROM pessoas');
    console.log(rows);
    return rows;
}

database.insertPessoa = async function(nome, idade, patrimonio){
    let [data] = await database.con.execute('INSERT into pessoas (nome, idade, patrimonio) values(?, ?, ?)', 
        [nome, idade, patrimonio]);
    console.log(data.insertId)
    return {'numero': data.insertId};
}

database.removePessoa = async function(id){
    let [data] = await database.con.execute('DELETE FROM pessoas where numero = ?', [id]);
    console.log('removido', data);
}

database.alteraPessoa = async function(id, nome, idade, patrimonio){
    let [data] = await database.con.execute('UPDATE pessoas SET nome = ?, idade = ?, patrimonio = ? where numero = ?',
        [nome, idade, patrimonio, id]);
    console.log('alterado', data);
}

export default database;

MySQL - Variáveis de ambiente

É importante não expor as informações de autenticação nos arquivos do seu repositório, como user e password do banco.

Para esconder, usamos variáveis de ambiente.

Instale o pacote dotenv
npm i dotenv

Crie um arquivo .env e adicione-o ao arquivo .gitignore, para que ele não seja enviado ao repositório junto aos commits.

MySQL - Variáveis de ambiente

.env
DB_HOST=localhost // aliases para serem chamados no código
DB_USER=root
DB_PASSWORD=
DB_DATABASE=loja
db.js
import 'dotenv/config'; // importando o dotenv

const pool = mysql.createPool({
  host: process.env.DB_HOST, // alias são chamados usando process.env
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

Dúvidas? 🤔

Exercícios

  1. Utilizando Node.js e Express, implemente uma API que resposta às 5 principais requisições HTTP (get, post, put, patch e delete), realizando essas ações em um banco de dados MySQL. O esquema do banco de dados fica a sua escolha. Para testar a API, utilize uma das ferramentas apresentadas.

--- <!-- _footer: https://www.w3schools.com/nodejs/nodejs_http.asp # Módulo HTTP - url A propriedade url armazena o endereço que vem após o nome do domínio, exemplo: http://localhost:8080/verao, a url irá retornar o /verao <figure> <figcaption>exemploURL.js</figcaption> ```js import http from 'http'; http.createServer((req, res) => { res.writeHead(200, {'Content-Type': 'text/html'}); res.write(req.url); res.end(); }).listen(8080); ``` </figure> --- <style scoped> pre { font-size: 0.8rem; } p { font-size: 0.8rem; } </style> <!-- _footer: https://developer.mozilla.org/pt-BR/docs/Web/API/URLSearchParams # Módulo HTTP – url Pode-se pegar os parâmetros da url utilizando o URLSearchParams. Link de exemplo: http://localhost:8080/?ano=2017&mes=Julho <figure> <figcaption>meuModulo.js</figcaption> ```js import http from 'http' http.createServer((req, res) => { res.writeHead(200, {'Content-Type': 'text/html'}); const param = new URLSearchParams(req.url.split('?')[1]); console.log(param); console.log(param.get('ano')); console.log(param.get('mes')); res.end(req.url); }).listen(8080); ``` </figure>

Com `async/await`, o código fica mais limpo, sem callbacks.

O `connection pool` é uma técnica para gerenciar conexões com o banco de dados de forma mais eficiente. Em vez de abrir e fechar uma conexão para cada consulta, um conjunto de conexões é mantido aberto e é reutilizado. Isso reduz a sobrecarga de estabelecer novas conexões, melhorando a performance e a escalabilidade da aplicação. <br> Criação de um pool de conexões: ```js const pool = mysql.createPool({ host: 'localhost', user: 'root', password: '', database: 'mydb' }); ```